[JavaScript 笔记] 神奇的闭包
date
Apr 19, 2022
slug
[JavaScript 笔记]神奇的闭包
status
Published
tags
JavaScript
summary
type
Post
什么是闭包(Closure)
闭包(Closure)是
Javascript 中一个特别重要的概念,是 JavaScript 有别于很多其他语言的一个神奇特性:对于一个定义好的函数,不论你在哪里调用它,它始终保持着对定义时的上下文的引用。看一个例子:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2示例中,
baz 函数不论在哪里被调用,执行结果打印的都是 foo 函数中声明的那个变量 a 。你会不会有这个疑问:变量 a 是函数 foo 的一个本地变量,当 foo 执行完成时,a 是不是应该被销毁?一般情况下,确实是这样的(其他语言也一样,
C, Java 都是)。在 JavaScript 中,如果没有形成闭包的话,函数本地变量确实会被销毁,不过示例中变量 a 属于闭包,所以它并不会被销毁。回到主题,什么是闭包?
MDN 对闭包这样定义:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。关于周围状态(词法环境),词法环境是从编译器角度来讲的,对于程序员,可以理解为程序执行上下文,也就是作用域链。
闭包与作用域
再来看个有趣的问题,我们稍微改动一下代码,也就是在上节示例的基础上,定义了另一个本地变量
b ,问题是:b 是否在闭包中?function foo() {
var a = 2;
var b = 1; // 定义了另一个本地变量
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2我们看看
Chrome 中的 debug 执行情况:
我们在
debug 环境下可以看到:b 是不在闭包中的。我们还看到,当前函数执行时会有一个上下文叫 作用域(Scope) ,并且其中包括:Local(本地), Closure(闭包), Global(全局) 三类作用域。看看
ECMAScript 规范对 Scope 的定义:
在 Table 9 — Internal Properties Only Defined for Some Objects 中,我们可以看到 [[Scope]] 的Value Type Domain 一列的值是 Lexical Environment ,这说明 [[Scope]] 就是一种词法环境,也就是函数执行上下文,作用域链。
《你不知道的 Javascript》上卷中第五章,把
闭包 描述为 作用域闭包 ,我觉得这是更为准确的描述,更接近本质。闭包与函数
再回去看看
闭包的定义:“一个函数和对其周围状态(lexical environment,词法环境)...”,本文先讨论了词法环境(作用域),因为我觉得要理解 闭包,先要理解 作用域,前面也说过,叫 作用域闭包 更能准确描述闭包的本质。那
闭包 和 函数 是什么关系,我认为 函数 是 闭包 形成的基本环境,这是其一。而且,这个函数不是全局函数,因为全局函数的词法环境是
Global ,而不是 Closure ,那么可以这么说:这个函数是一个嵌套定义的函数,这是其二。其三,我们可以从上节示例中看出,只有引用了外部函数中定义的变量才会出现在
闭包 中,也就是说:这个嵌套函数还必须引用外部函数中定义的变量。另外,
闭包 的神奇和强大还在于:Javascript 中函数是一等公民,可以作为参数、返回值、变量、对象属性值等,也就是 闭包 可以保存和传递。看一个场景案例(引用自 MDN):
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。
总结
最后,我们可以这样总结:
闭包 是一个绑定了外部作用域的嵌套函数。闭包产生的两个必要条件:
- 嵌套定义函数;
- 嵌套函数必须引用外部函数中定义的变量;